/************************************************************************
 *
 * Copyright (c) 2009
 * PrismTech Ltd.
 * All rights Reserved.
 *
 * LOGICAL_NAME:    SensorApp.java
 * FUNCTION:        OpenSplice DDS Power Tools Tutorial example code.
 * MODULE:          Tutorial for the Java programming language.
 * DATE             April 2009.
 ************************************************************************
 *
 * This class contains the implementation of the battlefield application.
 *
 ***/
package Sensor;

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;

import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSlider;
import javax.swing.JSpinner;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.SpinnerNumberModel;
import javax.swing.SwingUtilities;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

import track.BaseApplication;
import track.Constants;
import track.ErrorHandler;
import track.ObjectLocation;
import track.ObjectLocationDataReader;
import track.ObjectLocationSeqHolder;
import track.PointTrack;
import track.PointTrackDataWriter;
import track.Pos;
import DDS.ANY_INSTANCE_STATE;
import DDS.ANY_SAMPLE_STATE;
import DDS.ANY_VIEW_STATE;
import DDS.DataWriter;
import DDS.DataWriterQos;
import DDS.DataWriterQosHolder;
import DDS.HANDLE_NIL;
import DDS.LENGTH_UNLIMITED;
import DDS.SampleInfo;
import DDS.SampleInfoSeqHolder;
import Sensor.SensorAppWrapper.WrapperException;

public class SensorApp extends BaseApplication {
   // Interface elements.
   public static JFrame m_frame;

   private static JTextField m_strengthValue;

   private static JButton m_startButton;

   private static JButton m_stopButton;

   private static JButton m_exitButton;

   // The text area for output.
   protected static JTextArea m_outputArea;

   // The array list that stores the output.
   protected ArrayList<String> m_lines = new ArrayList<String>();

   // The maximum number of lines that will be stored/displayed.
   protected static int MAX_LINES = 20;

   // Default latency budget for the ObjectLocation data reader.
   public static double DEFAULT_OL_LATENCY_BUDGET = 0D;

   // Default transport priorities for airborne and surface tracked object
   // writers.
   public static int DEFAULT_AIR_TRANSPORT_PRIORITY = 2;

   public static int DEFAULT_SURFACE_TRANSPORT_PRIORITY = 4;

   // Default latency budgets for airborne and surface tracked object writers.
   public static double DEFAULT_AIR_LATENCY_BUDGET = 0D;

   public static double DEFAULT_SURFACE_LATENCY_BUDGET = 0D;

   // Delay (in milliseconds), based on the update frequency.
   public int m_updateDelay;

   // PointTrack message padding.
   public static int DEFAULT_AIR_PADDING = 0;

   public static String m_airPadding;

   public static int DEFAULT_SURFACE_PADDING = 0;

   public static String m_surfacePadding;

   // Reference to the data reader control.
   public static DataReaderControl m_drcObjectLocation;

   // References to the data writer controls.
   public static DataWriterControl m_airControl;

   public static DataWriterControl m_surfaceControl;

   // The ObjectLocation data reader reference.
   private ObjectLocationDataReader m_objLocDataReader;

   // The ObjectLocation data reader listener reference.
   private ObjectLocationDataReaderListenerImpl m_oldrListener;

   // The PointTrack data writer references.
   private PointTrackDataWriter m_pointTrackAirDataWriter;

   private PointTrackDataWriter m_pointTrackSurfaceDataWriter;

   // Flag to indicate if scanning should be performed or not.
   private boolean m_performScanning = false;

   // Map of information on published PointTrack instances.
   private HashMap<Integer, PointTrackInfo> m_published;

   /**
    * Stores the information for a published PointTrack instance. Includes a
    * reference to the data writer (airborne or surface) to avoid the need to
    * constantly determine which should be used.
    */
   class PointTrackInfo {
      private PointTrack m_pointTrack;

      private long m_handle = 0L;

      private PointTrackDataWriter m_writer;

      private boolean m_disposed = false;
   }

   /**
    * Listener implementation for the ObjectLocation data reader.
    */
   class ObjectLocationDataReaderListenerImpl extends
         SensorAppWrapper.ObjectLocationDataReaderListener {
      // Formatter used to control the display of the sensed objects.
      private DecimalFormat m_df = new DecimalFormat("0.00");

      // The time at which data was last handled in on_data_available ().
      private long m_lastProcess = -1;

      /**
       * Disposes and unregisters a PointTrack instance. Checks to ensure the
       * same instance is only processed once, and also removes the entry from
       * the published map.
       * 
       * @param info
       *            The PointTrackInfo to dispose and unregister
       */
      private void disposeAndUnregister(PointTrackInfo info) {
         int status;

         // Dispose and unregister from the appropriate data writer.
         if (info.m_disposed == false) {
            status = info.m_writer
                  .dispose(info.m_pointTrack, info.m_handle);
            ErrorHandler
                  .checkStatus(status,
                        "ObjectLocationDataReaderListenerImpl: on_data_available: dispose");
            status = info.m_writer.unregister_instance(info.m_pointTrack,
                  info.m_handle);
            ErrorHandler
                  .checkStatus(status,
                        "ObjectLocationDataReaderListenerImpl: on_data_available: unregister_instance");
            info.m_disposed = true;

            if (DEBUG) {
               output("Disposed/unregistered object ID "
                     + info.m_pointTrack.trackId);
            }

            // Remove from the map.
            Integer key = new Integer(info.m_pointTrack.trackId);
            Object oldData = m_published.remove(key);
            if (oldData == null) {
               output("Error: could not find map entry during disposal/unregister for key "
                     + key);
            }
         }
      }

      public void on_data_available(DDS.DataReader dataReader) {
         // If using an update frequency, ensure that the required interval
         // has elapsed.
         if ((m_lastProcess != -1) && (m_updateDelay != 0)) {
            if (System.currentTimeMillis() < (m_lastProcess + m_updateDelay)) {
               return;
            }
         }

         ObjectLocationDataReader olDataReader = (ObjectLocationDataReader) dataReader;
         ObjectLocationSeqHolder receivedData = new ObjectLocationSeqHolder();
         SampleInfoSeqHolder infoSeq = new SampleInfoSeqHolder();

         synchronized (m_syncObject) {
            int status = olDataReader.take(receivedData, infoSeq,
                  LENGTH_UNLIMITED.value, ANY_SAMPLE_STATE.value,
                  ANY_VIEW_STATE.value, ANY_INSTANCE_STATE.value);
            ErrorHandler
                  .checkStatus(status,
                        "ObjectLocationDataReaderListenerImpl: on_data_available: take");

            // Process the data if scanning is in operation.
            ObjectLocation[] locs = receivedData.value;
            SampleInfo[] samples = infoSeq.value;
            int count = 0;

            for (int i = 0; i < locs.length; i++) {
               ObjectLocation loc = locs[i];
               SampleInfo sample = samples[i];
               Integer mapKey = new Integer(loc.trackId);
               PointTrackInfo info = (PointTrackInfo) m_published
                     .get(mapKey);

               // Process "disposed" / "no longer alive" indications from
               // the battlefield.
               if (sample.instance_state != DDS.ALIVE_INSTANCE_STATE.value) {
                  if (DEBUG) {
                     output("Object ID "
                           + loc.trackId
                           + " has been disposed or is no longer available (instance state "
                           + sample.instance_state + ")");
                  }

                  // PointTrack instances will normally be
                  // disposed/unregistered once they move out of sensor
                  // range.
                  // Since the default size/position of the scope is
                  // entirely contained within the battlefield, an
                  // object leaving the battlefield has by definition
                  // already left sensor range. We dispose/unregister
                  // here for completeness.
                  //
                  // A null map entry is also valid at this point; this
                  // will happen for an object that never came
                  // within sensor range.
                  if (info != null) {
                     disposeAndUnregister(info);
                  }

                  // Processing complete for this instance.
                  continue;
               }

               float distance = calcDistance(Constants.SENSOR_X,
                     Constants.SENSOR_Y, Constants.SENSOR_Z, loc.x,
                     loc.y, loc.z);
               if (distance > Constants.SENSOR_RANGE) {
                  // Object has moved out of sensor range; dispose and
                  // unregister the instance. Strictly the
                  // dispose/unregister on PointTrack should result from a
                  // dispose/unregister on ObjectLocation,
                  // but doing this means objects get "stuck" on the
                  // display for the interval between leaving
                  // the sensor range and leaving the battlefield.
                  // Disposing/unregistering at this point removes
                  // the display, but relies on the knowledge that objects
                  // travel in a straight line and so once
                  // out of scope range will leave the battlefield without
                  // returning to within range.
                  if (info != null) {
                     disposeAndUnregister(info);
                  }
               } else {
                  // Object is within sensor range; publish the
                  // information to the PointTrack topic
                  // via the appropriate writer depending on altitude. We
                  // synchronise on the writer
                  // so that strength and priority changes aren't done at
                  // the same time.

                  count++;

                  // Create and register a PointTrack instance if needed.
                  if (info == null) {
                     info = new PointTrackInfo();
                     info.m_pointTrack = new PointTrack();
                     info.m_pointTrack.trackId = loc.trackId;
                     info.m_pointTrack.padding = (loc.z > 0 ? m_airPadding
                           : m_surfacePadding);
                     info.m_writer = (loc.z > 0 ? m_pointTrackAirDataWriter
                           : m_pointTrackSurfaceDataWriter);
                     m_published.put(mapKey, info);

                     info.m_handle = info.m_writer
                           .register_instance(info.m_pointTrack);
                     if (info.m_handle == HANDLE_NIL.value) {
                        output("Error registering "
                              + info.m_pointTrack.trackId);
                     }

                     if (DEBUG) {
                        output("Registered object ID "
                              + info.m_pointTrack.trackId);
                     }
                  }

                  // Update the position information and publish.
                  info.m_pointTrack.position = new Pos(distance,
                        calcDirection(Constants.SENSOR_X,
                              Constants.SENSOR_Y, loc.x, loc.y),
                        calcAngle(Constants.SENSOR_X,
                              Constants.SENSOR_Y, Constants.SENSOR_Z,
                              loc.x, loc.y, loc.z));
                  status = info.m_writer.write(info.m_pointTrack,
                        info.m_handle);
                  ErrorHandler
                        .checkStatus(status,
                              "ObjectLocationDataReaderListenerImpl.on_data_available: write");

                  StringBuffer sb = new StringBuffer();
                  sb.append(new Date());
                  sb.append(" -- ");
                  sb.append("ID: ");
                  sb.append(info.m_pointTrack.trackId);
                  sb.append(", DIST: ");
                  sb.append(m_df
                        .format(info.m_pointTrack.position.distance));
                  sb.append(", DIR: ");
                  sb.append(m_df
                        .format(info.m_pointTrack.position.direction));
                  sb.append(", ANG: ");
                  sb
                        .append(m_df
                              .format(info.m_pointTrack.position.angle));
                  outputToScreen(sb.toString());
               }
            }

            // Release resources.
            status = olDataReader.return_loan(receivedData, infoSeq);
            ErrorHandler
                  .checkStatus(status,
                        "ObjectLocationDataReaderListenerImpl: on_data_available: return_loan");

            // Record the time of last operation.
            m_lastProcess = System.currentTimeMillis();
         }
      }

      /**
       * Calculates the flat-plane direction between two points.
       */
      private float calcDirection(int x1, int y1, int x2, int y2) {
         int delta_y = y2 - y1;
         int delta_x = x2 - x1;
         float res = (float) Math.toDegrees(Math.atan((double) delta_y
               / delta_x));

         if (delta_x < 0) {
            res = 270 + res;
         } else if (delta_x > 0) {
            res = 90 + res;
         } else {
            if (delta_y < 0) {
               res = 0;
            } else {
               res = 180;
            }
         }

         return res;
      }

      /**
       * Calculates the distance between two points.
       */
      private float calcDistance(int x1, int y1, int z1, int x2, int y2,
            int z2) {
         float adjSq = (float) (Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
         float oppSq = (float) Math.pow(z2 - z1, 2);

         float distance = (float) Math.sqrt(adjSq + oppSq);

         return distance;
      }

      /**
       * Calculates the angle of elevation between two points.
       */
      private float calcAngle(int x1, int y1, int z1, int x2, int y2, int z2) {
         float adj = (float) Math.sqrt(Math.pow(x2 - x1, 2)
               + Math.pow(y2 - y1, 2));
         float opp = (float) (z2 - z1);

         float angle = (float) Math.toDegrees(Math.atan((double) opp / adj));

         return angle;
      }
   }

   public SensorApp(String[] args) {
      parseArguments(args);

      if (DEBUG) {
         output("Scope positioned at (" + Constants.SENSOR_X + ","
               + Constants.SENSOR_Y + "," + Constants.SENSOR_Z
               + "), range " + Constants.SENSOR_RANGE);
      }

      // Create the map for the published instance information.
      m_published = new HashMap<Integer, PointTrackInfo>();

      // Obtain a reference to the ObjectLocation data reader.
      m_objLocDataReader = SensorAppWrapper.BattlefieldParticipantWrapper.SubscriberWrapper.ObjectLocationDataReaderWrapper
            .getDataReader();

      // Obtain a reference to the PointTrack data writers.
      m_pointTrackAirDataWriter = SensorAppWrapper.ParticipantWrapper.PublisherWrapper.PointTrackAirDataWriterWrapper
            .getDataWriter();
      m_pointTrackSurfaceDataWriter = SensorAppWrapper.ParticipantWrapper.PublisherWrapper.PointTrackSurfaceDataWriterWrapper
            .getDataWriter();

      // Create the ObjectLocation data reader listener.
      m_oldrListener = new ObjectLocationDataReaderListenerImpl();
   }

   public void startScanning() {
      if (!m_performScanning) {
         output("Starting scanning");
         try {
            SensorAppWrapper.BattlefieldParticipantWrapper.SubscriberWrapper.ObjectLocationDataReaderWrapper
                  .attach(m_oldrListener);
         } catch (WrapperException e) {
            System.err.println("Cannot attach listener: " + e.getMessage());
         }
         
         m_performScanning = true;
      }
   }

   public void stopScanning() {
      // If scanning is not in operation, and Battlefield is running,
      // instances will keep
      // accumulating in the ObjectLocation topic and we rely on the default
      // topic QoS's
      // (volatile durability and best-effort reliability) to handle resource
      // management.

      if (m_performScanning) {
         output("Stopping scanning");
         try {
            SensorAppWrapper.BattlefieldParticipantWrapper.SubscriberWrapper.ObjectLocationDataReaderWrapper
                  .detach(m_oldrListener);
         } catch (WrapperException e) {
            System.err.println("Cannot detach listener: " + e.getMessage());
         }
         
         m_performScanning = false;
      }
   }

   private void setOwnershipStrength(int strength) {
      output("Setting ownership strength to " + strength);
      m_strengthValue.setText(Integer.toString(strength));

      setWriterStrength(m_pointTrackAirDataWriter, strength);
      setWriterStrength(m_pointTrackSurfaceDataWriter, strength);
   }

   private void setWriterStrength(DataWriter writer, int strength) {
      synchronized (m_syncObject) {
         DataWriterQosHolder holder = new DataWriterQosHolder();
         int status = writer.get_qos(holder);
         ErrorHandler.checkStatus(status,
               "SensorApplication.setWriterStrength: get_qos");

         DataWriterQos qos = holder.value;
         qos.ownership_strength.value = strength;
         status = writer.set_qos(qos);
         ErrorHandler.checkStatus(status,
               "SensorApplication.setWriterStrength: set_qos");
      }
   }

   public String toString() {
      return "Sensor";
   }

   protected synchronized void setMessagePaddingMember(int padding,
         DataWriterControl control) {
      if (control == m_airControl) {
         m_airPadding = createPadding(padding);
      } else {
         m_surfacePadding = createPadding(padding);
      }

      output(control.getTitle() + ": message padding set to " + padding);
      control.messagePaddingSet();
   }

   public static void main(String[] args) {
      // Initialise the Splice application.
      try
      {
      SensorAppWrapper.start();
      } catch(WrapperException e)
      {
         System.err.println("Cannot start application: " + e.getMessage());
      }

      // Initialise the application.
      final SensorApp instance = new SensorApp(args);

      //
      // Create the display components.
      //

      // Output panel: displays the tracked objects that the sensor has
      // detected.
      JPanel outputPanel = new JPanel();
      outputPanel.setBorder(BorderFactory.createTitledBorder(m_bevelBorder,
            "Detected Objects"));
      outputPanel.setPreferredSize(new Dimension(600, 300));
      outputPanel.setLayout(new BorderLayout());
      m_outputArea = new JTextArea();
      m_outputArea.setEditable(false);
      JScrollPane sp = new JScrollPane(m_outputArea);
      outputPanel.add(sp, BorderLayout.CENTER);

      // Strength slider.
      JSlider strengthSlider = new JSlider(JSlider.HORIZONTAL, 0, 10, 0);
      strengthSlider.setMajorTickSpacing(5);
      strengthSlider.setMinorTickSpacing(1);
      strengthSlider.setPaintTicks(true);
      strengthSlider.setPaintLabels(true);
      strengthSlider.setSnapToTicks(true);

      JPanel sliderPanel = new JPanel();
      sliderPanel.setLayout(new BorderLayout(10, 5));
      sliderPanel.setBorder(BorderFactory.createTitledBorder(m_bevelBorder,
            "Ownership Strength"));
      sliderPanel.add(strengthSlider, BorderLayout.CENTER);

      // Strength value indicator.
      m_strengthValue = new JTextField(2);
      m_strengthValue.setEditable(false);
      JPanel valuePanel = new JPanel();
      valuePanel.setLayout(new BorderLayout());
      valuePanel.setBorder(m_bevelBorder);
      valuePanel.add(m_strengthValue, BorderLayout.CENTER);
      sliderPanel.add(valuePanel, BorderLayout.WEST);

      // Data reader control.
      m_drcObjectLocation = instance.new DataReaderControl(
            instance.m_objLocDataReader, "ObjectLocation Data Reader",
            DEFAULT_OL_LATENCY_BUDGET);
      JPanel rp = new JPanel(new GridLayout(1, 2));
      rp.add(m_drcObjectLocation.getPanel());
      rp.add(sliderPanel);

      // Data writer controls.
      m_airControl = instance.new DataWriterControl(
            instance.m_pointTrackAirDataWriter,
            "PointTrack DataWriter (Air Objects)",
            DEFAULT_AIR_LATENCY_BUDGET, DEFAULT_AIR_TRANSPORT_PRIORITY,
            DEFAULT_AIR_PADDING);
      m_surfaceControl = instance.new DataWriterControl(
            instance.m_pointTrackSurfaceDataWriter,
            "PointTrack DataWriter (Surface Objects)",
            DEFAULT_SURFACE_LATENCY_BUDGET,
            DEFAULT_SURFACE_TRANSPORT_PRIORITY, DEFAULT_SURFACE_PADDING);
      JPanel wp = new JPanel(new GridLayout(1, 2));
      wp.add(m_airControl.getPanel());
      wp.add(m_surfaceControl.getPanel());

      // Settings panel: update frequency control. We set the spinner to the
      // minimum value,
      // and not the default, so we can perform initialisation using
      // setValue() later.
      JPanel updatePanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
      updatePanel.setBorder(BorderFactory.createTitledBorder(m_bevelBorder,
            "Sensor Settings"));
      JLabel updateLabel = new JLabel("Update Frequency (Hz)");
      SpinnerNumberModel updateModel = new SpinnerNumberModel(
            MINIMUM_UPDATE_FREQUENCY, MINIMUM_UPDATE_FREQUENCY,
            MAXIMUM_UPDATE_FREQUENCY, UPDATE_FREQUENCY_INTERVAL);
      JSpinner updateSpinner = new JSpinner(updateModel);
      updateSpinner.setPreferredSize(m_controlSize);
      updatePanel.add(updateLabel);
      updatePanel.add(updateSpinner);

      // Buttons.
      JButton clearButton = new JButton("Clear");
      m_startButton = new JButton("Start");
      m_stopButton = new JButton("Stop");
      m_exitButton = new JButton("Exit");
      JPanel innerButtonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
      innerButtonPanel.add(clearButton);
      innerButtonPanel.add(m_startButton);
      innerButtonPanel.add(m_stopButton);
      innerButtonPanel.add(m_exitButton);
      JPanel buttonPanel = new JPanel(new BorderLayout(10, 5));
      buttonPanel.add(innerButtonPanel, BorderLayout.SOUTH);

      // Combine JVM, update frequency and buttons panels.
      JPanel combined1 = new JPanel(new BorderLayout(10, 5));
      combined1.add(updatePanel, BorderLayout.NORTH);
      combined1.add(buttonPanel, BorderLayout.SOUTH);
      JPanel combined = new JPanel(new GridLayout(1, 2));
      combined.add(instance.m_jvmControl.getPanel());
      combined.add(combined1);

      // Control panel.
      JPanel controlPanel = new JPanel(new BorderLayout(10, 5));
      controlPanel.add(rp, BorderLayout.NORTH);
      controlPanel.add(wp, BorderLayout.SOUTH);

      // Add button action listeners.
      clearButton.addActionListener(new ActionListener() {
         public void actionPerformed(ActionEvent ae) {
            instance.clearScreenDisplay();
         }
      });
      m_startButton.addActionListener(new ActionListener() {
         public void actionPerformed(ActionEvent ae) {
            instance.startScanning();
            m_startButton.setEnabled(false);
            m_stopButton.setEnabled(true);
         }
      });
      m_stopButton.addActionListener(new ActionListener() {
         public void actionPerformed(ActionEvent ae) {
            instance.stopScanning();
            m_startButton.setEnabled(true);
            m_stopButton.setEnabled(false);
         }
      });
      m_exitButton.addActionListener(new ActionListener() {
         public void actionPerformed(ActionEvent ae) {
            m_startButton.setEnabled(false);
            m_stopButton.setEnabled(false);
            m_exitButton.setEnabled(false);
            instance.stopScanning();
            instance.stopJVMInformationUpdates();
            synchronized (instance) {
               instance.notify();
            }
         }
      });

      // Add slider action listener.
      strengthSlider.addChangeListener(new ChangeListener() {
         public void stateChanged(ChangeEvent ce) {
            JSlider source = (JSlider) ce.getSource();
            if (!source.getValueIsAdjusting()) {
               instance.setOwnershipStrength(source.getValue());
            }
         }
      });

      // Add spinner action listener.
      updateSpinner.addChangeListener(new ChangeListener() {
         public void stateChanged(ChangeEvent ce) {
            JSpinner spinner = (JSpinner) ce.getSource();
            Double val = (Double) spinner.getValue();
            double freq = val.doubleValue();

            instance.m_updateDelay = (freq == 0.0 ? 0 : (int) (1000 / freq));
            instance.output("Update delay set to " + instance.m_updateDelay
                  + " (frequency " + freq + "Hz)");
         }
      });

      // Set up initial interface state.
      m_stopButton.doClick();
      strengthSlider.setValue(1);
      m_drcObjectLocation.setLatencyBudget(DEFAULT_OL_LATENCY_BUDGET);
      m_airControl.setTransportPriority(DEFAULT_AIR_TRANSPORT_PRIORITY);
      m_airControl.setLatencyBudget(DEFAULT_AIR_LATENCY_BUDGET);
      m_airControl.setMessagePadding(DEFAULT_AIR_PADDING);
      m_surfaceControl
            .setTransportPriority(DEFAULT_SURFACE_TRANSPORT_PRIORITY);
      m_surfaceControl.setLatencyBudget(DEFAULT_SURFACE_LATENCY_BUDGET);
      m_surfaceControl.setMessagePadding(DEFAULT_SURFACE_PADDING);
      updateSpinner.setValue(DEFAULT_UPDATE_FREQUENCY);

      //
      // Construct the frame.
      //

      m_frame = new JFrame(instance.toString());
      m_frame.addWindowListener(new WindowAdapter() {
         public void windowClosing(WindowEvent e) {
            m_exitButton.doClick();
         }
      });

      m_frame.getContentPane().setLayout(new BorderLayout(10, 10));
      m_frame.getContentPane().add(outputPanel, BorderLayout.NORTH);
      m_frame.getContentPane().add(controlPanel, BorderLayout.CENTER);
      m_frame.getContentPane().add(combined, BorderLayout.SOUTH);

      m_frame.pack();
      m_frame.setVisible(true);

      // Automatically start if required.
      if (AUTO_START) {
         launchStartThread(5000L, m_startButton);
      }

      // stop scanning and exit if timeout is higher than 0
      // seconds)
      if (runWithTimeOut) {

         // Trigger a wait on the application to keep active.
         instance.triggerWait(TIME_OUT);

         instance.stopScanning();
         instance.stopJVMInformationUpdates();

         synchronized (instance) {
            instance.notify();
         }

      } else {
         instance.triggerWait();
      }

      // Stop the Splice application.
      try {
         SensorAppWrapper.stop();
      } catch (WrapperException e) {
         System.err.println("Cannot stop application: " + e.getMessage());
      }

      // Complete.
      System.out.println("SensorApplication.main :: exiting");
      m_frame.dispose();
   }

   /**
    * Outputs a string to the screen display.
    * 
    * @param message
    *            String to display
    */
   private void outputToScreen(String message) {
      synchronized (m_lines) {
         while (m_lines.size() > MAX_LINES) {
            m_lines.remove(0);
         }
         m_lines.add(message);
      }

      SwingUtilities.invokeLater(new TextAreaUpdater());
   }

   /**
    * Runnable to update the text area.
    */
   private class TextAreaUpdater implements Runnable {
      public void run() {
         synchronized (m_lines) {
            m_outputArea.setText("");
            for (int i = 0; i < m_lines.size(); i++) {
               m_outputArea.append(m_lines.get(i) + "\n");
            }
            m_outputArea.setCaretPosition(m_outputArea.getDocument()
                  .getLength());
         }
      }
   }

   /**
    * Clears the currently-displayed information.
    */
   protected void clearScreenDisplay() {
      SwingUtilities.invokeLater(new TextAreaClearer());
   }

   /**
    * Runnable to clear the text area.
    */
   private class TextAreaClearer implements Runnable {
      public void run() {
         synchronized (m_lines) {
            m_lines.clear();
         }
         m_outputArea.setText("");
      }
   }
}
